package com.xingole.xspring.web.servlet;

import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.PropertyValues;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;

/**
 * <p>Simple extension of {@code jakarta.servlet.http.HttpServlet} which treats its config parameter(init-param
 * entries within the servlet tag in {@code web.xml}) as <b>bean properties</b></p>
 *
 * <p><b>A handy superclass for any type of servlet.</b></p>
 *
 * <p>Type conversion of config parameters is automatic, with the corresponding setter method getting invoked with the converted value.
 *   It is also possible for subclasses to specify required properties.Parameters without matching bean property setter
 *   will simply be ignored.</p>
 *
 * <p>This servlet leaves request handling to subclasses, inheriting the default of {@code HttpServlet}({@code doGet,
 * doPost}, etc).</p>
 *
 * <p>This generic servlet base class has <b>NO</b> dependency on the Spring {@code org.springframework.context.ApplicationContext}
 * concept.Simple servlets usually don't load their own context but rather access service beans from the Spring root
 * application context, accessible via the filter's ServletContext.</p>
 *
 *
 */
public abstract class HttpServletBean extends HttpServlet {

    // This set is used to save the required properties (init-param) by the specific subclasses.
    private final Set<String> requiredProperties = new HashSet<>(4);

    /**
     * <p>Subclasses can invoke this method to specify that this property (which must match a JavaBean property they
     * expose) is mandatory, and must be supplied as a config parameter.This should be called from the constructor
     * of a subclasses.</p>
     *
     * <p>This method is only relevant in case of traditional initialization driven by a ServletConfig instance.</p>
     * @param property
     */
    protected final void addRequiredProperty(String property) {
        this.requiredProperties.add(property);
    }

    /**
     * <p>Map config parameters onto bean properties of this servlet, and invoke subclasses initialization.</p>
     *
     * @throws ServletException - if bean properties are invalid (or required properties are missing), or if subclass
     * initialization fails.
     */
    @Override
    public final void init() throws ServletException {
        // Set bean properties from init parameters.
        PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
        // TODO:more work need to do.

        // Let subclasses do whatever initialization they like.
        initServletBean();
    }

    /**
     * <p>Subclasses may override this to perform custom initialization.
     *    All bean properties of this servlet will have been set before this method is invoked.</p>
     *
     * <p>This default implementation is empty.</p>
     * @throws ServletException - if subclass initialization fails
     */
    protected void initServletBean() throws ServletException {
        // The method provides implementation templates for specific subclasses.
    }

    /**
     * PropertyValues implementation created from ServletConfig init parameters.
     */
    private static class ServletConfigPropertyValues extends MutablePropertyValues {

        /**
         * Create new ServletConfigPropertyValues.
         * @param config - the ServletConfig we'll use to take PropertyValues from
         *               (remember that the type of value in init-param is String).
         * @param requiredProperties - set of property names we need, where we can't accept default values(String type).
         * @throws ServletException -  if any required properties are missing.
         */
        public ServletConfigPropertyValues(ServletConfig config, Set<String> requiredProperties)
                throws ServletException{

            // the set of missing property
            // Initially, we should set the missing property set the same as the required properties set if not empty;
            // With the processing of adding property value's operation, we should remove the corresponding
            // property from the missing property set.
            Set<String> missingProps = (!CollectionUtils.isEmpty(requiredProperties) ?
                    new HashSet<>(requiredProperties) : null);

            // the enumeration collection of the init-param names for this servlet
            Enumeration<String> paramNames = config.getInitParameterNames();
            // iterate the param, namely servlet bean's property
            while(paramNames.hasMoreElements()) {
                String property = paramNames.nextElement();
                String value = config.getInitParameter(property);
                /*
                 * Add a PropertyValue object, replacing any existing one for the
                 * corresponding property or getting merged with it (if applicable).
                 */
                addPropertyValue(new PropertyValue(property, value));

                // remove the meeting property from the missing property set.
                if(missingProps != null) {
                    missingProps.remove(property);
                }
            }

            // Fails if we are still missing properties.
            if(!CollectionUtils.isEmpty(missingProps)) {
                throw new ServletException(
                        "Initilization from ServletConfig for servlet '" + config.getServletName() +
                                "' failed; the following required properties were missing: " +
                                StringUtils.collectionToDelimitedString(missingProps, " , ")  // using the utils class for specific framework
                );
            }
        }
    }

    /**
     * Overridden method that simply returns null when no ServletConfig set yet.
     * @return : the name of this servlet
     */
    @Override
    public String getServletName() {
        return (getServletConfig() != null ?
                    getServletConfig().getServletName() : null
                );
    }
}
